# encoding: utf-8
"""
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    created by lane.chang on '13/05/2024'
    comment: 业务逻辑
"""
from itertools import groupby
import asyncio
from datetime import datetime as dt, datetime
from datetime import timedelta

from sqlalchemy import func, text
from sqlalchemy.exc import IntegrityError
import pandas as pd

from project.lib.common import okex_client, with_session, okex_trade_client
from project.third_part.okex import Inst, KLine, get_decimal_places, OkexException
from project.model.digiccy import AnnVol, ConfigInfo, AlgoOrder, OrderLog
from project.service.common import CommonService


class DigiccyService:

    @staticmethod
    @with_session()
    def sync_trading_mode(session=None):
        """ 同步交易模式
        :param session:
        :return:
        """
        config_info = session.query(ConfigInfo).filter(ConfigInfo.code == 'trading_mode').first()
        if config_info and config_info.value == 'simulate':
            okex_client.switch_model('1')

    @staticmethod
    def get_instruments() -> list[Inst]:
        """
        :return:
        """
        data = okex_client.get_instruments()
        insts = []
        for rec in data:
            inst = Inst()
            inst.inst_id = rec['instId']
            inst.inst_code = rec['instIdCode'] if -2147483648 < rec['instIdCode'] < 2147483647 else 0
            inst.list_time = rec['listTime']
            inst.state = rec['state']

            insts.append(inst)

        return insts

    @staticmethod
    def get_kline_data_inst(inst_id: str, date: str | None = None, bar='5m',
                            start_time: datetime | None = None,
                            end_time: datetime | None = None) -> list[KLine]:
        """ 获取指定币种的日K线(5M)
        :param inst_id: 指定产品ID
        :param date: 指定日期
        :param bar: 粒度
        :param start_time:
        :param end_time:
        :param reverse:
        :return:
        """
        start_timestamp = end_timestamp = ''
        if date:
            start_time = dt.strptime(f'{date} 00:00:00', '%Y-%m-%d %H:%M:%S')
            end_time = dt.strptime(f'{date} 23:59:59', '%Y-%m-%d %H:%M:%S')
        if start_time:
            start_timestamp = int(start_time.timestamp()) * 1000 - 1
        if end_time:
            end_timestamp = int(end_time.timestamp()) * 1000
        if not start_timestamp and not end_timestamp:
            return []

        klines = []
        _end_timestamp = end_timestamp
        while True:
            kline_data = okex_client.get_kline_data(instid=inst_id, before=start_timestamp, after=_end_timestamp, bar=bar)
            if not kline_data:
                break
            for rec in kline_data:
                kline = KLine()
                kline.inst_id = inst_id
                kline.trading_ts = rec[0]
                kline.opening_price_fm = float(rec[1])
                kline.highest_price_fm = float(rec[2])
                kline.lowest_price_fm = float(rec[3])
                kline.closing_price_fm = float(rec[4])
                klines.append(kline)
                _end_timestamp = str(int(kline.trading_ts) - 1)

            if len(kline_data) < 300:
                break

        return klines

    @staticmethod
    async def get_daily_kline_inst(inst_id, date: str) -> list[KLine]:
        """ 获取指定产品日K线数据(1D)
        :param inst_id: 指定产品ID
        :param date: 指定日期
        :return:
        """
        date = dt.strptime(date, '%Y-%m-%d')

        # 那天 00:00 与 24:00 的毫秒时间戳
        ts_start = int(date.timestamp()) * 1000
        ts_end = ts_start + 24 * 3600 * 1000

        kline_data = okex_client.get_kline_data(instid=inst_id, before=ts_start - 1, after=ts_end, bar='1D', limit=1)

        klines = []
        for rec in kline_data:
            # # 过滤掉未完结的
            # if rec[8] == '0':
            #     continue
            kline = KLine()
            kline.inst_id = inst_id
            kline.trading_ts = rec[0]
            kline.opening_price = float(rec[1])
            kline.highest_price = float(rec[2])
            kline.lowest_price = float(rec[3])
            kline.closing_price = float(rec[4])
            kline.trading_vol = float(rec[5])
            kline.trading_vol_ccy = float(rec[6])
            kline.trading_vol_quote = float(rec[7])
            kline.confirm = rec[8]
            klines.append(kline)

        return klines

    @staticmethod
    async def get_daily_kline(date: str) -> list[KLine]:
        """ 获取所有币种的日K线
        :return:
        """
        # 获取当前时间所有产品列表
        insts = DigiccyService.get_instruments()
        inst_code_map = {v.inst_id: v.inst_code for v in insts if v.state == 'live'}

        tasks = list()
        count = 0
        klines = []
        for idx, inst in enumerate(insts):
            # if inst.inst_id in ('MINA-USDT-SWAP', 'KAITO-USDT-SWAP', 'ORDER-USDT-SWAP', 'THETA-USDT-SWAP', 'APR-USDT-SWAP'):
            #     continue
            tasks.append(
                asyncio.create_task(DigiccyService.get_daily_kline_inst(inst_id=inst.inst_id, date=date), name=inst.inst_id)
            )
            # 每次 30 条并发请求
            if idx // 40 > count:
                await asyncio.gather(*tasks)
                for task in tasks:
                    klines.extend(task.result())
                tasks.clear()
                count = idx // 40

                await asyncio.sleep(2.1)  # 延迟1秒
                continue

        if tasks:
            await asyncio.gather(*tasks)
            for task in tasks:
                klines.extend(task.result())

        for kline in klines:
            # 赋值产品编号
            kline.inst_code = inst_code_map.get(kline.inst_id, '')

        return klines

    @staticmethod
    def flush_daily_kline_history():
        """ 刷新所有币种的日K线(历史数据)
        :return:
        """
        # 获取当前时间所有产品列表
        insts = DigiccyService.get_instruments()

        now = dt.now()
        before_timestamp = dt.strptime((now - timedelta(days=90)).strftime('%Y-%m-%d') + ' 00:00:00', '%Y-%m-%d %H:%M:%S').timestamp()
        before = str(int(before_timestamp) * 1000 - 1)

        after_timestamp = dt.strptime(now.strftime('%Y-%m-%d') + ' 00:00:00', '%Y-%m-%d %H:%M:%S').timestamp()
        after = str(int(after_timestamp) * 1000 - 1)

        print(before, after)
        count = 0
        for inst in insts:
            # 只刷上线超过90天的币种
            if inst.validity_period < 90:
                continue

            klines = []
            data = okex_client.get_kline_data_history(instid=inst.inst_id, before=before, after=after, bar='1D')
            count += 1
            for rec in data:
                kline = KLine()
                kline.inst_id = inst.inst_id
                kline.inst_code = inst.inst_code
                kline.trading_ts = rec[0]
                kline.opening_price = float(rec[1])
                kline.highest_price = float(rec[2])
                kline.lowest_price = float(rec[3])
                kline.closing_price = float(rec[4])
                kline.trading_vol = float(rec[5])
                kline.trading_vol_ccy = float(rec[6])
                kline.trading_vol_quote = float(rec[7])
                kline.confirm = rec[8]
                klines.append(kline)

            DigiccyService.flush_annvol_db(klines)

            DigiccyService.flush_annvol(inst_id=inst.inst_id)

    @staticmethod
    @with_session()
    def flush_annvol_db(klines: list[KLine], session=None):
        """ 将波动率数据更新至db
        :return:
        """
        if not klines:
            return
        # 按照时间戳排序
        klines.sort(key=lambda x: (x.inst_id, x.trading_ts))
        #
        for kline in klines:
            ann_vol = session.query(AnnVol).filter(AnnVol.inst_code == kline.inst_code, AnnVol.trading_ts == kline.trading_ts).first()
            if not ann_vol:
                ann_vol = AnnVol()
            ann_vol.trading_time = trading_time = dt.fromtimestamp(int(int(kline.trading_ts) / 1000))
            ann_vol.date = trading_time.date()
            ann_vol.inst_id = kline.inst_id
            ann_vol.inst_code = kline.inst_code
            ann_vol.trading_ts = kline.trading_ts
            ann_vol.opening_price = kline.opening_price
            ann_vol.highest_price = kline.highest_price
            ann_vol.lowest_price = kline.lowest_price
            ann_vol.closing_price = kline.closing_price
            ann_vol.trading_vol = kline.trading_vol
            ann_vol.trading_vol_ccy = kline.trading_vol_ccy
            ann_vol.trading_vol_quote = kline.trading_vol_quote

            try:
                session.add(ann_vol)
                session.flush()
            except IntegrityError:
                session.rollback()

        session.commit()

    @staticmethod
    @with_session()
    def flush_annvol(inst_id='', date='', session=None):
        """ 刷新年化波动率
        :param inst_id: 指定产品
        :param date: 指定日期
        :param session:
        :return:
        """
        query = session.query(AnnVol)
        if inst_id:
            query = query.filter(AnnVol.inst_id == inst_id)
        if date:
            query = query.filter(AnnVol.date == date)
        data = query.order_by(AnnVol.inst_code, AnnVol.trading_ts).all()

        for annvol in data:
            annvol.d_flu = annvol.highest_price - annvol.lowest_price  # 每日波幅(最高价 - 最低价)
            annvol.vf = 14  # 波动系数
            annvol.avc = 19.1  # 年化波动系数
            # 14日波幅
            start_date = annvol.date - timedelta(days=13)
            fourteen_flu = (
                session.query(func.sum(AnnVol.highest_price - AnnVol.lowest_price)).
                filter(AnnVol.inst_code == annvol.inst_code, AnnVol.date.between(start_date, annvol.date)).scalar()
            )
            annvol.fourteen_flu = fourteen_flu
            # 平均波幅(14天波幅总和 / 14)
            annvol.atr = fourteen_flu / annvol.vf
            # 每日波动率 = 平均波幅 / 收盘价
            annvol.d_vol = annvol.atr / annvol.closing_price
            # 年化波动率 = 每日波动率 * 年化波动系数
            annvol.ann_vol = annvol.d_vol * annvol.avc

        session.commit()

    @staticmethod
    async def import_annvol_data(date: str):
        """
        :return:
        """
        # 下载请求日K线数据
        klines = await DigiccyService.get_daily_kline(date=date)

        # 刷新入库
        DigiccyService.flush_annvol_db(klines)

        # 刷新年化波动率
        DigiccyService.flush_annvol(date=date)

    @staticmethod
    @with_session()
    def get_annvol_data(inst_id='', date='', session=None):
        """
        :param inst_id:
        :param date:
        :param session:
        :return:
        """
        # 币种的有效期
        insts = DigiccyService.get_instruments()
        inst_ids = [v.inst_id for v in insts]
        inst_id_map = {v.inst_id: v for v in insts}

        query = session.query(AnnVol)
        if inst_id:
            query = query.filter(AnnVol.inst_id == inst_id)
        if date:
            query = query.filter(AnnVol.date == date)
        query = query.filter(AnnVol.inst_id.in_(inst_ids))
        data = query.order_by(AnnVol.ann_vol.desc()).all()

        records = []
        rank = 0
        for idx, annvol in enumerate(data):

            # 产品信息
            inst = inst_id_map.get(annvol.inst_id)
            if not inst:
                continue
            # 币种有效期
            validity_period = inst.validity_period if inst.validity_period < 90 else 90
            # 上线未满 14天, 不计入查询范围
            if validity_period < 14:
                continue
            rank += 1
            records.append(
                [
                    {'header': '日期', 'align': 'center', 'value': annvol.date.strftime('%Y-%m-%d')},
                    {'header': '排名', 'align': 'center', 'value': rank},
                    {'header': '名称', 'value': annvol.inst_id},
                    {'header': '有效期', 'align': 'right', 'value': validity_period},
                    {'header': '开盘价', 'align': 'right', 'value': annvol.opening_price},
                    {'header': '最高价', 'align': 'right', 'value': annvol.highest_price},
                    {'header': '最低价', 'align': 'right', 'value': annvol.lowest_price},
                    {'header': '收盘价', 'align': 'right', 'value': annvol.closing_price},
                    {'header': '每日波幅', 'align': 'right', 'value': annvol.d_flu},
                    {'header': '14日波幅', 'align': 'right', 'value': annvol.fourteen_flu},
                    {'header': '平均振幅', 'align': 'right', 'value': annvol.atr},
                    {'header': '每日波动率', 'align': 'right', 'value': annvol.d_vol, 'is_percent': True},
                    {'header': '年化波动率', 'align': 'right', 'value': annvol.ann_vol, 'is_percent': True}
                ]
            )

        return records

    @staticmethod
    @with_session()
    def get_annvol_data_days(inst_id='', date='', session=None, days=14) -> dict[str, list[AnnVol]]:
        """ 取得14日
        :param inst_id:
        :param date:
        :param session:
        :param days:
        :return:
        """
        query = session.query(AnnVol)
        if inst_id:
            query = query.filter(AnnVol.inst_id == inst_id)
        if date:
            date = dt.strptime(date, '%Y-%m-%d').date()
            start_date = date - timedelta(days=days)
            query = query.filter(AnnVol.date.between(start_date, date))
        data = query.order_by(AnnVol.inst_id, AnnVol.date).all()

        ret_data = {key: list(group) for key, group in groupby(data, key=lambda x: x.inst_id)}

        return ret_data

    @staticmethod
    @with_session()
    def get_server_time(session=None):
        """
        :return:
        """
        server_now = session.scalar(text("SELECT NOW()"))  # Pos
        return server_now

    @staticmethod
    @with_session()
    def get_max_annvol_date(session=None):
        """
        :return:
        """
        rec = session.query(func.max(AnnVol.date)).first()

        return rec[0]

    @staticmethod
    def export_annvol(date: str, full_path: str, inst_id='', ):
        """
        :param date: 指定年化率的日期
        :param inst_id: 指定产品
        :param full_path: 文件输出全路径
        :return:
        """
        # 取得记录
        records = DigiccyService.get_annvol_data(date=date, inst_id=inst_id)
        if not records:
            return
        headers = [v['header'] for v in records[0]]

        data = {}
        for header in headers:
            _data = []
            for rec in records:
                for v in rec:
                    if v['header'] != header:
                        continue
                    _data.append(v['value'])
            data[header] = _data
        df = pd.DataFrame(data)

        # 14日年化率数据
        fourteen_data = DigiccyService.get_annvol_data_days(date=date, inst_id=inst_id, days=90)

        with pd.ExcelWriter(full_path,
                            engine='xlsxwriter',
                            datetime_format='yyyy-mm-dd',
                            date_format='yyyy-mm-dd') as writer:

            df.to_excel(writer, sheet_name='年化率', index=False)

            # 拿到 workbook 和 worksheet 对象
            workbook = writer.book
            worksheet = writer.sheets['年化率']

            # 设置列宽
            worksheet.set_column('A:A', 12)  # 日期
            worksheet.set_column('B:B', 6)  # 排名
            worksheet.set_column('C:C', 30)  # 名称
            worksheet.set_column('D:D', 8)  # 有效期
            worksheet.set_column('E:E', 13)  # 开盘价
            worksheet.set_column('F:F', 13)  # 最高价
            worksheet.set_column('G:G', 13)  # 最低价
            worksheet.set_column('H:H', 13)  # 收盘价
            worksheet.set_column('I:I', 13)  # 每日波幅
            worksheet.set_column('J:J', 13)  # 14日波幅
            worksheet.set_column('K:K', 13)  # 平均振幅
            worksheet.set_column('L:L', 13)  # 每日波动率
            worksheet.set_column('M:M', 13)  # 年化波动率

            # 表头加粗
            header_fmt = workbook.add_format({'bold': True})
            worksheet.set_row(0, None, header_fmt)

            # 子sheet
            for inst_id in df['名称']:
                inst_data = fourteen_data.get(inst_id)
                if not inst_data:
                    continue

                # 转化为DataFrame形式
                inst_df = pd.DataFrame(
                    {
                        '日期': [annvol.date.strftime('%Y-%m-%d') for annvol in inst_data],
                        '开盘价': [annvol.opening_price for annvol in inst_data],
                        '最高价': [annvol.highest_price for annvol in inst_data],
                        '最低价': [annvol.lowest_price for annvol in inst_data],
                        '收盘价': [annvol.closing_price for annvol in inst_data],
                        '每日波幅': [annvol.d_flu for annvol in inst_data],
                        '14日波幅': [annvol.fourteen_flu for annvol in inst_data],
                        '平均振幅': [annvol.atr for annvol in inst_data],
                        '每日波动率': [annvol.d_vol for annvol in inst_data],
                        '年化波动率': [annvol.ann_vol for annvol in inst_data]
                    }
                )

                inst_df.to_excel(writer, sheet_name=inst_id, index=False)
                worksheet = writer.sheets[inst_id]
                # 设置列宽
                worksheet.set_column('A:A', 12)  # 日期
                worksheet.set_column('B:B', 13)  # 开盘价
                worksheet.set_column('C:C', 13)  # 最高价
                worksheet.set_column('D:D', 13)  # 最低价
                worksheet.set_column('E:E', 13)  # 收盘价
                worksheet.set_column('F:F', 13)  # 每日波幅
                worksheet.set_column('G:G', 13)  # 14日波幅
                worksheet.set_column('H:H', 13)  # 平均振幅
                worksheet.set_column('I:I', 13)  # 每日波动率
                worksheet.set_column('J:J', 13)  # 年化波动率

                # 表头加粗
                header_fmt = workbook.add_format({'bold': True})
                worksheet.set_row(0, None, header_fmt)

    @staticmethod
    def export_kline_fm(full_path: str, start_time, end_time, inst_id_auto='', inst_id_self=''):
        """ 导出5M日K线
        :param start_time: 开始时间
        :param end_time: 结束时间
        :param inst_id_auto: 指定产品
        :param inst_id_self: 自选产品
        :param full_path: 文件输出全路径
        :return:
        """
        df_auto = df_self = None
        if inst_id_auto:
            klines_auto = DigiccyService.get_kline_data_inst(inst_id=inst_id_auto, start_time=start_time, end_time=end_time)
            if klines_auto:
                records = CommonService.convert_klines_to_table_fm(klines_auto)
                headers = [v['header'] for v in records[0]]

                data = {}
                for header in headers:
                    _data = []
                    for rec in records:
                        for v in rec:
                            if v['header'] != header:
                                continue
                            _data.append(v['value'])
                    data[header] = _data
                df_auto = pd.DataFrame(data)

        if inst_id_self:
            klines_self = DigiccyService.get_kline_data_inst(inst_id=inst_id_self, start_time=start_time, end_time=end_time)
            if klines_self:
                records = CommonService.convert_klines_to_table_fm(klines_self)
                headers = [v['header'] for v in records[0]]

                data = {}
                for header in headers:
                    _data = []
                    for rec in records:
                        for v in rec:
                            if v['header'] != header:
                                continue
                            _data.append(v['value'])
                    data[header] = _data
                df_self = pd.DataFrame(data)

        if df_auto is None and df_self is None:
            return

        with pd.ExcelWriter(full_path,
                            engine='xlsxwriter',
                            datetime_format='yyyy-mm-dd',
                            date_format='yyyy-mm-dd') as writer:

            sheet_names = list()
            if df_auto is not None:
                sheet_name = inst_id_auto
                df_auto.to_excel(writer, sheet_name=sheet_name, index=False)
                sheet_names.append(sheet_name)

            if df_self is not None:
                sheet_name = inst_id_self
                df_self.to_excel(writer, sheet_name=sheet_name, index=False)
                sheet_names.append(sheet_name)

            for sheet_name in sheet_names:
                # 拿到 workbook 和 worksheet 对象
                workbook = writer.book
                worksheet = writer.sheets[sheet_name]

                # # 设置列宽
                worksheet.set_column('A:A', 16)  # 时间
                worksheet.set_column('B:B', 10)  # 5M开盘
                worksheet.set_column('C:C', 10)  # 5M收盘
                worksheet.set_column('D:D', 10)  # 5M最低
                worksheet.set_column('E:E', 10)  # 5M最高
                worksheet.set_column('F:F', 10)  # 今日最低
                worksheet.set_column('G:G', 10)  # 今日最高
                worksheet.set_column('H:H', 10)  # 点阵负值
                worksheet.set_column('I:I', 10)  # 点阵正值
                worksheet.set_column('J:J', 10)  # 点阵终值
                worksheet.set_column('K:K', 10)  # 状态
                worksheet.set_column('L:L', 80)  # 每日波动率

                # 表头加粗
                header_fmt = workbook.add_format({'bold': True})
                worksheet.set_row(0, None, header_fmt)

    @staticmethod
    @with_session()
    def get_config_info(code: str, session=None):
        """ 取得配置信息
        :param code:
        :param session:
        :return:
        """
        config_info = session.query(ConfigInfo).filter(ConfigInfo.code == code).first()

        return config_info.value if config_info else ''

    @staticmethod
    @with_session()
    def update_config_info(code, value, session=None):
        """ 更新配置信息
        :param code:
        :param value:
        :param session:
        :return:
        """
        config_info = session.query(ConfigInfo).filter(ConfigInfo.code == code).first()
        if not config_info:
            config_info = ConfigInfo()
            session.add(config_info)

        config_info.code = code
        config_info.value = value

        session.commit()

    @staticmethod
    @with_session()
    def get_config_infos(codes: list[str], session=None):
        """
        :param codes:
        :param session:
        :return:
        """
        config_infos = session.query(ConfigInfo).filter(ConfigInfo.code.in_(codes)).all()
        config_info_map = {v.code: v.value for v in config_infos}

        return {v: config_info_map.get(v, '') for v in codes}

    @staticmethod
    @with_session()
    def update_config_infos(data: dict, session=None):
        """ 更新配置信息
        :param data: {code: xxx, value: xxx}
        :param session:
        :return:
        """
        config_infos = session.query(ConfigInfo).filter(ConfigInfo.code.in_(list(data.keys()))).all()
        config_info_map = {v.code: v for v in config_infos}

        for code, value in data.items():
            config_info = config_info_map.get(code)
            if not config_info:
                config_info = ConfigInfo()
                session.add(config_info)

            config_info.code = code
            config_info.value = value

        session.commit()

    @staticmethod
    @with_session()
    def add_algo_order(inst_id: str, algo_price: str, side: str, pos_side: str, annualized_volatility_id=0, session=None):
        """ 添加委托订单
        :param inst_id:
        :param algo_price:
        :param side: sell/buy
        :param pos_side: long/short
        :param annualized_volatility_id: 年化波动率关联id
        :param session:
        :return:
        """
        # 开仓量
        opening_position_volume = DigiccyService.get_config_info(code='opening_position_volume')
        if not opening_position_volume:
            opening_position_volume = '300'

        # 误差范围
        trigger_scope = DigiccyService.get_config_info(code='trigger_scope')
        if not trigger_scope:
            trigger_scope = '0.01'
        # 张数
        lots = okex_client.get_convert_contract_coin(inst_id=inst_id, sz=opening_position_volume, px=algo_price)
        if not lots:
            raise Exception('未换算出张数')

        # 计算触发价格
        dec_places = get_decimal_places(algo_price)
        # 开多
        if pos_side == 'long':
            trigger_price = round(float(algo_price) + float(algo_price) * float(trigger_scope) / 100, dec_places)
        # 开空
        else:
            trigger_price = round(float(algo_price) - float(algo_price) * float(trigger_scope) / 100, dec_places)

        # 下单完成
        try:
            result = okex_trade_client.place_algo_order(inst_id=inst_id, trigger_px=trigger_price, order_px=trigger_price, sz=str(lots[0]['sz']), side=side, pos_side=pos_side)
        except OkexException as ex:
            # 记录失败日志
            OrderLog.create_modify(
                session,
                order_type='algo_order',
                inst_id=inst_id,
                algo_price=float(algo_price),
                side=side,
                pos_side=pos_side,
                annualized_volatility_id=annualized_volatility_id,
                fail_code=ex.code,
                fail_msg=ex.msg,
                params={
                    'instId': inst_id,
                    'triggerPx': trigger_price,
                    'orderPx': trigger_price,
                    'sz': str(lots[0]['sz']),
                    'side': side,
                    'posSide': pos_side
                }
            )
            return

        algo_id = result['data'][0]['algoId']
        data = okex_trade_client.get_algo_order(algo_id=algo_id)
        if not data:
            return

        # 获取当前时间所有产品列表
        insts = DigiccyService.get_instruments()
        inst_code_map = {v.inst_id: v.inst_code for v in insts if v.state == 'live'}

        rec = data[0]
        algo_id = rec['algoId']
        algo_order = session.query(AlgoOrder).filter(AlgoOrder.algo_id == algo_id).first()
        if not algo_order:
            algo_order = AlgoOrder()
            algo_order.algo_id = algo_id
            session.add(algo_order)
            session.flush()

        algo_order.inst_id = inst_id
        algo_order.inst_code = inst_code_map.get(inst_id, 0)
        algo_order.count = rec['sz']  # 委托数量
        algo_order.side = rec['side']  # 订单方向
        algo_order.pos_side = rec['posSide']  # 持仓方向 开多/开空
        algo_order.lever = int(rec['lever']) if rec['lever'] else 1
        algo_order.trigger_price = float(rec['triggerPx']) if rec['triggerPx'] else 0  # 委托触发价格
        algo_order.trigger_type = rec['triggerPxType']  # 计划委托触发价格类型
        algo_order.trigger_ts = trigger_ts = rec['triggerTime']
        if trigger_ts != '0':
            algo_order.trigger_time = dt.fromtimestamp(int(int(rec['triggerTime']) / 1000))
        algo_order.algo_price = float(rec['ordPx']) if rec['ordPx'] else 0  # 委托价格
        algo_order.last_price = float(rec['last']) if rec['last'] else 0  # 下单时最新成交价
        algo_order.state = rec['state']  # 订单状态
        algo_order.ctime_ts = rec['cTime']
        algo_order.ctime = ctime = dt.fromtimestamp(int(int(rec['cTime']) / 1000))
        algo_order.date = ctime.date()
        algo_order.utime_ts = rec['uTime']
        algo_order.utime = dt.fromtimestamp(int(int(rec['uTime']) / 1000))

        # 记录策略委托日志表
        OrderLog.create_modify(
            session,
            order_type='algo_order',
            inst_id=inst_id,
            algo_price=float(algo_price),
            side=side,
            pos_side=pos_side,
            annualized_volatility_id=annualized_volatility_id,
            algo_id=algo_order.algo_id,
            params={
                'instId': inst_id,
                'triggerPx': trigger_price,
                'orderPx': trigger_price,
                'sz': str(lots[0]['sz']),
                'side': side,
                'posSide': pos_side
            },
            commit=False
        )

        session.commit()

    @staticmethod
    @with_session()
    def auto_algo_order(date: str, session=None):
        """ 自动委托
        :param date:
        :param session:
        :return:
        """
        annvols = DigiccyService.get_annvol_data(date=date)
        if not annvols:
            return
        # 年化率排名第一的产品
        annvol = annvols[0]
        annvol_dict = {v['header']: v['value'] for v in annvol}

        # 检查自动委托的开关
        algo_auto_switch = DigiccyService.get_config_info(code='algo_auto_switch')
        if algo_auto_switch and algo_auto_switch == 'false':
            return

        # 根据年化率数据进行自动策略委托
        algo_order = session.query(AlgoOrder).filter(AlgoOrder.date == date).first()
        if algo_order:
            # 已自动委托, 退出
            return

        annualized_volatility = session.query(AnnVol).filter(AnnVol.inst_id == annvol_dict['名称'], AnnVol.date == annvol_dict['日期']).first()
        # 最高价开多
        DigiccyService.add_algo_order(inst_id=annvol_dict['名称'],
                                      algo_price=str(annvol_dict['最高价']),
                                      side='buy',
                                      pos_side='long',
                                      annualized_volatility_id=annualized_volatility.id if annualized_volatility else 0)

        # 最低价开空
        DigiccyService.add_algo_order(inst_id=annvol_dict['名称'],
                                      algo_price=str(annvol_dict['最低价']),
                                      side='sell',
                                      pos_side='short',
                                      annualized_volatility_id=annualized_volatility.id if annualized_volatility else 0)

    @staticmethod
    @with_session()
    def get_algo_orders(inst_id='', session=None) -> list[AlgoOrder]:
        """ 取得策略委托订单记录
        :param inst_id:
        :param session:
        :return:
        """
        query = session.query(AlgoOrder)
        if inst_id:
            query = session.query(AlgoOrder.inst_id == inst_id)
        query = query.filter(AlgoOrder.state != 'canceled')
        algo_orders = query.order_by(AlgoOrder.id.desc()).all()

        return algo_orders or []

    @staticmethod
    @with_session()
    def cancel_algo_orders(session=None):
        """ 撤销所有委托单
        :return:
        """
        algo_orders = session.query(AlgoOrder).filter(AlgoOrder.state != 'canceled').order_by(AlgoOrder.id.desc()).all()
        if not algo_orders:
            return

        for algo_order in algo_orders:
            # 从欧易平台获取委托订单信息
            order = okex_trade_client.get_algo_order(algo_id=algo_order.algo_id)
            if not order:
                continue

            if order[0]['state'] in ('canceled', 'effective'):
                algo_order.state = order[0]['state']
                continue

            try:
                fail_code = fail_msg = ''
                cancel_order = okex_trade_client.cancel_algo_order(inst_id=algo_order.inst_id, algo_id=algo_order.algo_id)
                if not cancel_order:
                    continue
            except OkexException as ex:
                fail_code = ex.code
                fail_msg = ex.msg

            # 记录日志
            OrderLog.create_modify(
                session,
                order_type='cancel_algo',
                inst_id=algo_order.inst_id,
                fail_code=fail_code,
                fail_msg=fail_msg,
                params=[{'instId': algo_order.inst_id, 'algoId': algo_order.algo_id}],
                commit=False
            )
            algo_order.state = 'canceled'

        session.commit()

    @staticmethod
    @with_session()
    def close_position(session=None):
        """ 平仓
        :return:
        """
        # 获得当前仓位信息
        positions = okex_trade_client.get_positions()
        for pos in positions:
            fail_code = fail_msg = ''
            try:
                okex_trade_client.close_position(inst_id=pos.inst_id, pos_side=pos.pos_side, margin_model=pos.margin_model, ccy=pos.ccy)
            except OkexException as ex:
                fail_code = ex.code
                fail_msg = ex.msg

            algo_orders = session.query(AlgoOrder).filter(AlgoOrder.inst_id == pos.inst_id, AlgoOrder.state != 'canceled').all()
            for algo_order in algo_orders:
                algo_order.state = 'canceled'

            # 记录失败日志
            OrderLog.create_modify(
                session,
                order_type='close_position',
                inst_id=pos.inst_id,
                pos_side=pos.pos_side,
                fail_code=fail_code,
                fail_msg=fail_msg,
                params={
                    'instId': pos.inst_id,
                    'posSide': pos.pos_side,
                    'mgnMode': pos.margin_model,
                    'ccy': pos.ccy
                },
                commit=False
            )

        session.commit()


if __name__ == "__main__":
    """
    """
    # 同步交易模式
    # DigiccyService.sync_trading_mode()

    # DigiccyService.add_algo_order(inst_id='BTC-USDT-SWAP', algo_price='116400.3', side='buy', pos_side='long')

    # asyncio.run(DigiccyService.import_annvol_data(date='2025-11-11'))

    CommonService.flush_okex_trade_client()
    #
    # DigiccyService.auto_algo_order(date='2025-11-11')
    #
    DigiccyService.cancel_algo_orders()
    #
    DigiccyService.close_position()




